Skip to content

Python实现简易搜索引擎

学习目标

  • 理解搜索引擎的基本原理
  • 学习使用Python构建简易倒排索引
  • 掌握基本的文本预处理技术
  • 实现简单但高效的本地搜索功能

搜索引擎的核心原理

在深入代码实现之前,我们需要理解搜索引擎的核心原理——倒排索引(Inverted Index)。

什么是倒排索引?

传统的正向索引是从文档到内容的映射:

文档1 -> 内容 ("Python是一种易于学习的编程语言")
文档2 -> 内容 ("编程语言有很多种类")
文档3 -> 内容 ("学习Python可以提升编程能力")

而倒排索引则是从词项到文档的映射:

Python -> [文档1, 文档3]
编程 -> [文档1, 文档2, 文档3]
语言 -> [文档1, 文档2]
学习 -> [文档1, 文档3]
...

倒排索引使我们能够快速找到包含特定词项的所有文档,这正是搜索的核心需求。

文本预处理步骤

在建立倒排索引前,我们需要对文本进行预处理:

  1. 分词(Tokenization):将文本拆分为单词或短语
  2. 去除停用词(Stop Words Removal):移除常见且对搜索无意义的词(如"的"、"是"、"和"等)
  3. 词干提取(Stemming):将不同形式的词归一化(如"running"、"runs"都变为"run")
  4. 词形还原(Lemmatization):类似词干提取,但更精确地将词还原为其基本形式

开始实现我们的简易搜索引擎

我们将使用纯Python实现一个简单的搜索引擎,包含以下功能:

  • 文档加载和预处理
  • 倒排索引构建
  • 简单的查询处理
  • 结果排序

第一步:环境准备

python
# 安装必要的库
# pip install nltk

import os
import re
import math
import json
from collections import defaultdict, Counter
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer

# 下载必要的NLTK资源
nltk.download('punkt')
nltk.download('stopwords')

第二步:文本预处理函数

python
def preprocess_text(text):
    """对文本进行预处理,包括分词、去停用词和词干提取"""
    # 转为小写
    text = text.lower()
    
    # 分词
    tokens = word_tokenize(text)
    
    # 去除标点和数字
    tokens = [token for token in tokens if token.isalpha()]
    
    # 去除停用词
    stop_words = set(stopwords.words('english'))
    tokens = [token for token in tokens if token not in stop_words]
    
    # 词干提取
    stemmer = PorterStemmer()
    tokens = [stemmer.stem(token) for token in tokens]
    
    return tokens

第三步:构建倒排索引

python
class SimpleSearchEngine:
    def __init__(self):
        self.documents = {}  # 文档存储:{doc_id: 原始文本}
        self.index = defaultdict(list)  # 倒排索引:{term: [doc_id1, doc_id2, ...]}
        self.term_frequencies = defaultdict(Counter)  # 词频统计:{doc_id: {term: frequency}}
        self.document_lengths = {}  # 文档长度:{doc_id: 长度}
        self.total_docs = 0  # 文档总数
    
    def add_document(self, doc_id, text):
        """添加文档到搜索引擎"""
        self.documents[doc_id] = text
        self.total_docs += 1
        
        # 预处理文本
        tokens = preprocess_text(text)
        
        # 计算词频
        term_freq = Counter(tokens)
        self.term_frequencies[doc_id] = term_freq
        self.document_lengths[doc_id] = len(tokens)
        
        # 更新倒排索引
        for term in set(tokens):  # 使用集合去重
            self.index[term].append(doc_id)
    
    def build_index_from_directory(self, directory):
        """从目录中加载文档并构建索引"""
        doc_id = 0
        for filename in os.listdir(directory):
            if filename.endswith('.txt'):
                with open(os.path.join(directory, filename), 'r', encoding='utf-8') as f:
                    text = f.read()
                    self.add_document(doc_id, text)
                    doc_id += 1
        print(f"已加载 {doc_id} 个文档并构建索引")

第四步:实现搜索功能

python
    def search(self, query, top_k=5):
        """搜索查询,返回相关性最高的top_k个文档"""
        # 预处理查询
        query_tokens = preprocess_text(query)
        
        # 计算相关性分数 (使用TF-IDF加权的余弦相似度)
        scores = defaultdict(float)
        
        for term in query_tokens:
            if term in self.index:
                # 计算IDF (Inverse Document Frequency)
                idf = math.log(self.total_docs / len(self.index[term]))
                
                # 更新包含该词的文档分数
                for doc_id in self.index[term]:
                    # TF (Term Frequency)
                    tf = self.term_frequencies[doc_id][term]
                    
                    # TF-IDF权重
                    scores[doc_id] += tf * idf
        
        # 对分数进行归一化处理
        for doc_id in scores:
            # 避免除以零
            if self.document_lengths[doc_id] > 0:
                scores[doc_id] /= self.document_lengths[doc_id]
        
        # 排序并返回前K个结果
        sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
        
        results = []
        for doc_id, score in sorted_scores[:top_k]:
            # 提取匹配片段用于展示
            snippet = self.get_snippet(doc_id, query_tokens)
            results.append({
                'doc_id': doc_id,
                'score': score,
                'snippet': snippet,
                'full_text': self.documents[doc_id]
            })
        
        return results
    
    def get_snippet(self, doc_id, query_tokens, context_size=25):
        """提取包含查询词的文本片段"""
        text = self.documents[doc_id]
        
        # 寻找最佳匹配位置
        best_position = 0
        max_matches = 0
        
        tokens = text.lower().split()
        
        for i in range(len(tokens)):
            matches = sum(1 for term in query_tokens if term in tokens[i:i+10])
            if matches > max_matches:
                max_matches = matches
                best_position = i
        
        # 获取上下文
        start = max(0, best_position - context_size)
        end = min(len(tokens), best_position + context_size + 10)
        
        snippet = " ".join(tokens[start:end])
        
        # 添加省略号表示截断
        if start > 0:
            snippet = "..." + snippet
        if end < len(tokens):
            snippet = snippet + "..."
            
        return snippet

第五步:完整示例和使用方法

python
# 使用示例
if __name__ == "__main__":
    # 创建搜索引擎实例
    search_engine = SimpleSearchEngine()
    
    # 添加一些示例文档
    search_engine.add_document(0, "Python是一种易于学习的编程语言,被广泛应用于数据分析和人工智能领域。")
    search_engine.add_document(1, "编程语言有很多种类,包括C++、Java、Python等。")
    search_engine.add_document(2, "学习Python可以提升你的编程能力,尤其是在数据科学方面。")
    search_engine.add_document(3, "人工智能技术正在快速发展,深度学习是其中的重要分支。")
    search_engine.add_document(4, "数据分析需要使用各种工具和技术,Python是其中最受欢迎的。")
    
    # 测试搜索
    query = "Python 数据分析"
    results = search_engine.search(query)
    
    print(f"查询: '{query}'")
    print(f"找到 {len(results)} 个相关文档:\n")
    
    for i, result in enumerate(results):
        print(f"结果 {i+1} (得分: {result['score']:.4f}):")
        print(f"片段: {result['snippet']}")
        print("---")

运行效果

执行上述代码,你会看到类似如下的输出:

查询: 'Python 数据分析'
找到 5 个相关文档:

结果 1 (得分: 0.6753):
片段: ...python是一种易于学习的编程语言,被广泛应用于数据分析和人工智能领域。
---
结果 2 (得分: 0.5428):
片段: ...数据分析需要使用各种工具和技术,python是其中最受欢迎的。
---
结果 3 (得分: 0.3214):
片段: ...学习python可以提升你的编程能力,尤其是在数据科学方面。
---

简易搜索引擎的优化方向

我们的简易搜索引擎已经实现了基本功能,但仍有多个优化方向:

  1. 性能优化

    • 使用更高效的数据结构存储索引
    • 实现增量索引更新
    • 引入多线程或异步处理
  2. 功能扩展

    • 支持中文分词(如使用jieba)
    • 添加拼写纠错
    • 实现查询扩展(近义词、同义词)
    • 支持更复杂的查询语法(AND、OR、NOT)
  3. 排序改进

    • 引入BM25排序算法
    • 考虑文档新鲜度
    • 添加用户反馈机制

实战练习:构建本地文件搜索系统

目标:使用我们的简易搜索引擎创建一个能够搜索本地文本文件的应用

步骤

  1. 准备一个包含多个文本文件的目录
  2. 使用SimpleSearchEngine加载并索引这些文件
  3. 实现一个简单的命令行界面,允许用户输入查询
  4. 展示搜索结果,并允许用户查看完整文档

代码示例

python
import os
import argparse

def main():
    parser = argparse.ArgumentParser(description='本地文件搜索工具')
    parser.add_argument('--dir', type=str, required=True, help='要索引的文件目录')
    args = parser.parse_args()
    
    # 初始化搜索引擎
    engine = SimpleSearchEngine()
    
    # 构建索引
    print(f"正在索引目录 {args.dir} 中的文件...")
    engine.build_index_from_directory(args.dir)
    
    # 交互式搜索循环
    while True:
        query = input("\n输入搜索查询 (输入'quit'退出): ")
        if query.lower() == 'quit':
            break
            
        results = engine.search(query, top_k=5)
        
        if not results:
            print("未找到匹配结果。")
            continue
            
        print(f"\n找到 {len(results)} 个相关文档:\n")
        
        for i, result in enumerate(results):
            print(f"[{i+1}] 得分: {result['score']:.4f}")
            print(f"片段: {result['snippet']}")
            print("---")
        
        # 查看完整文档
        while True:
            choice = input("\n输入编号查看完整文档 (输入'n'继续搜索): ")
            if choice.lower() == 'n':
                break
                
            try:
                index = int(choice) - 1
                if 0 <= index < len(results):
                    print("\n" + "="*60)
                    print(f"文档内容 #{results[index]['doc_id']}:")
                    print(results[index]['full_text'])
                    print("="*60)
                else:
                    print("无效的选择。")
            except ValueError:
                print("请输入有效的数字或'n'。")

if __name__ == "__main__":
    main()

小结

在本节中,我们学习了如何使用Python实现一个简易的搜索引擎,包括:

  1. 搜索引擎的核心原理——倒排索引
  2. 文本预处理的基本步骤
  3. 如何构建和查询倒排索引
  4. 使用TF-IDF为搜索结果评分
  5. 如何将简易搜索引擎应用于实际场景

这个简易搜索引擎虽然功能有限,但它包含了搜索引擎的核心概念,为我们后续学习更复杂的搜索工具打下了基础。

思考题

  1. 我们的简易搜索引擎使用了TF-IDF算法进行排序,请思考如何改进这个排序算法,使其更符合用户的搜索期望?
  2. 如何扩展我们的搜索引擎,使其支持PDF、Word等非纯文本格式的文档?
  3. 我们的实现在处理大量文档时可能会遇到内存问题,如何改进代码以处理GB级别的文档集合?

在下一节中,我们将学习如何使用专业的搜索库——Whoosh,来构建更高效的本地搜索系统。